package cn.fanyamin.util.collection;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import cn.fanyamin.util.MathUtils;

/**
 * The Class TimingSumValueList.
 * 
 * @author yafan
 */
public class TimingSumValueList implements Iterable<Long>, Serializable {

    /** serialVersionUID. */
    private static final long serialVersionUID = 786757174863308826L;

    /** The Constant FIVE_MINUTES. */
    public static final int FIVE_MINUTES = 300000;

    /** The Constant MAX_SIZE. */
    public static final int MAX_SIZE = 12;

    /** The value max size. */
    private final int valueMaxSize;

    /** The value start time. */
    private long valueStartTime;

    /** The values. */
    protected final RingBuffer<Long> values;

    /**
     * Instantiates a new timing sum value list.
     */
    public TimingSumValueList() {
        valueMaxSize = MAX_SIZE;
        values = new RingBuffer<Long>(12);
    }

    /**
     * Instantiates a new timing sum value list.
     * 
     * @param size
     *            the size
     */
    public TimingSumValueList(int size) {
        valueMaxSize = size;
        values = new RingBuffer<Long>(valueMaxSize);
    }

    /**
     * Insert value.
     * 
     * @param timestamp
     *            the timestamp
     * @param val
     *            the val
     */
    public void insertValue(long timestamp, long val) {
        if (0 == this.valueStartTime) {
            this.valueStartTime = MathUtils.divideAndFloor(timestamp,
                    FIVE_MINUTES) * FIVE_MINUTES;
        }
        long distanceTime = timestamp - valueStartTime;

        int idx = 0;
        if (distanceTime < 0) {
            return;
        }

        if (distanceTime == 0) {
            updateMetrics(idx, val);
            return;
        }
        long distanceCnt = MathUtils.divideAndFloor(distanceTime, FIVE_MINUTES);

        if (distanceCnt >= valueMaxSize * 2) {
            values.clear();
            valueStartTime = MathUtils.divideAndFloor(timestamp, FIVE_MINUTES)
                    * FIVE_MINUTES;

            values.enqueue(val);
        } else {
            // distance > 1 hour, need discard some metrics cells
            if (distanceCnt >= valueMaxSize) {
                // System.out.println("distanceCnt=" + distanceCnt+
                // ", cellCount=" + cellCount);
                for (int i = valueMaxSize; i <= distanceCnt; i++) {
                    if (!values.isEmpty()) {
                        values.dequeue();
                    }
                    valueStartTime += FIVE_MINUTES;
                }

                idx = valueMaxSize - 1;
            } else {
                // distance <= 1 hour
                idx = (int) (distanceCnt);

            }
            updateMetrics(idx, val);
        }

    }

    /**
     * Update metrics.
     * 
     * @param idx
     *            the idx
     * @param val
     *            the val
     */
    protected void updateMetrics(int idx, long val) {
        Long oldVal = values.get(idx);
        if (null == oldVal) {
            values.insert(idx, val);
            return;
        }
        oldVal += val;
        values.insert(idx, oldVal);
    }

    /**
     * Gets the values.
     * 
     * @param beginTime
     *            the begin time
     * @return the values
     */
    public List<Long> getValues(long beginTime) {
        List<Long> ret = new ArrayList<Long>(valueMaxSize);
        for (int i = 0; i < valueMaxSize; i++) {
            long thisBeginTime = beginTime + i * FIVE_MINUTES;
            ret.add(i, this.getValue(thisBeginTime));
        }
        return ret;
    }

    /**
     * Gets the values.
     * 
     * @return the values
     */
    public List<Long> getValues() {
        return this.values.getList();
    }

    /**
     * Gets the value.
     * 
     * @param theTime
     *            the the time
     * @return the value
     */
    public Long getValue(Long theTime) {
        long distanceTime = theTime - valueStartTime;
        if (distanceTime == 0) {
            return this.values.get(0);
        } else if (distanceTime < 0) {
            return null;
        }
        // else distanceTime > 0
        long distanceCnt = MathUtils.divideAndFloor(distanceTime, FIVE_MINUTES);
        if (distanceCnt >= valueMaxSize) {
            return null;
        }
        return this.values.get((int) distanceCnt);
    }

    /**
     * Clear.
     */
    public void clear() {
        this.valueStartTime = 0;
        this.values.clear();
    }

    /**
     * @return iterator
     * 
     * @see java.lang.Iterable#iterator()
     */
    public Iterator<Long> iterator() {
        return this.values.iterator();
    }

    /**
     * Sets the value start time.
     * 
     * @param timestamp
     *            the new value start time
     */
    public void setValueStartTime(long timestamp) {
        long oldStartTime = this.valueStartTime;
        this.valueStartTime = timestamp;
        long distanceCnt = MathUtils.divideAndFloor(this.valueStartTime
                - oldStartTime, FIVE_MINUTES);
        for (int i = 0; i < distanceCnt; i++) {
            values.add(null);
        }
    }

    /**
     * @return a string
     * 
     * @see java.lang.Object#toString()
     */
    public String toString() {
        return this.values.toString() + ", valueStartTime=" + valueStartTime;
    }

    /**
     * Size.
     * 
     * @return the int
     */
    public int size() {
        return this.values.size();
    }
}
